package sim.app.geo.haiti;


import java.util.ArrayList;
import java.util.HashMap;
import sim.app.geo.haiti.HaitiFood.Node;
import sim.field.network.Edge;



public class AStar
{

    /**
     * Assumes that both the start and end location are NODES as opposed to
     * LOCATIONS
     * <p/>
     * @param world
     * @param start
     * @param goal
     * <p/>
     * @return
     */
    public ArrayList<Location> astarPath(HaitiFood world, Node start, Node goal)
    {

        // initial check
        if (start == null || goal == null)
        {
            System.out.println("Error: invalid node provided to AStar");
        }

        // containers for the metainformation about the Nodes relative to the
        // A* search
        HashMap<Node, AStarNodeWrapper> foundNodes =
            new HashMap<Node, AStarNodeWrapper>();


        AStarNodeWrapper startNode = new AStarNodeWrapper(start);
        AStarNodeWrapper goalNode = new AStarNodeWrapper(goal);
        foundNodes.put(start, startNode);
        foundNodes.put(goal, goalNode);

        startNode.gx = 0;
        startNode.hx = heuristic(start, goal);
        startNode.fx = heuristic(start, goal);

        // A* containers: nodes to be investigated, nodes that have been investigated
        ArrayList<AStarNodeWrapper> closedSet = new ArrayList<AStarNodeWrapper>(),
            openSet = new ArrayList<AStarNodeWrapper>();
        openSet.add(startNode);


        while (openSet.size() > 0)
        { // while there are reachable nodes to investigate

            AStarNodeWrapper x = findMin(openSet); // find the shortest path so far
            if (x.node == goal)
            { // we have found the shortest possible path to the goal!
                // Reconstruct the path and send it back.
                return reconstructPath(goalNode);
            }
            openSet.remove(x); // maintain the lists
            closedSet.add(x);

            // check all the neighbors of this location
            for (Edge l : x.node.links)
            {

                Node n = (Node) l.from();
                if (n == x.node)
                {
                    n = (Node) l.to();
                }

                // get the A* meta information about this Node
                AStarNodeWrapper nextNode;
                if (foundNodes.containsKey(n))
                {
                    nextNode = foundNodes.get(n);
                } else
                {
                    nextNode = new AStarNodeWrapper(n);
                    foundNodes.put(n, nextNode);
                }

                if (closedSet.contains(nextNode)) // it has already been considered
                {
                    continue;
                }

                // otherwise evaluate the cost of this node/edge combo
                double tentativeCost = x.gx + (Integer) l.info;
                boolean better = false;

                if (!openSet.contains(nextNode))
                {
                    openSet.add(nextNode);
                    nextNode.hx = heuristic(n, goal);
                    better = true;
                } else if (tentativeCost < nextNode.gx)
                {
                    better = true;
                }

                // store A* information about this promising candidate node
                if (better)
                {
                    nextNode.cameFrom = x;
                    nextNode.gx = tentativeCost;
                    nextNode.fx = nextNode.gx + nextNode.hx;
                }
            }
        }

        return null;
    }


    /**
     * Takes the information about the given node n and returns the path that
     * found it.
     * <p/>
     * @param n the end point of the path
     * <p/>
     * @return an ArrayList of nodes that lead from the given Node to the Node
     *         from which the search began
     */
    ArrayList<Location> reconstructPath(AStarNodeWrapper n)
    {
        ArrayList<Location> result = new ArrayList<Location>();
        AStarNodeWrapper x = n;
        while (x.cameFrom != null)
        {
            result.add(0, x.node.loc); // add this edge to the front of the list
            x = x.cameFrom;
        }

        return result;
    }


    /**
     * Measure of the estimated distance between two Nodes.
     * <p/>
     * @return notional "distance" between the given nodes.
     */
    double heuristic(Node x, Node y)
    {
        return x.loc.distanceTo(y.loc);
    }


    /**
     * Considers the list of Nodes open for consideration and returns the node
     * with minimum fx value
     * <p/>
     * @param set list of open Nodes
     * <p/>
     * @return
     */
    AStarNodeWrapper findMin(ArrayList<AStarNodeWrapper> set)
    {
        double min = 100000;
        AStarNodeWrapper minNode = null;
        for (AStarNodeWrapper n : set)
        {
            if (n.fx < min)
            {
                min = n.fx;
                minNode = n;
            }
        }
        return minNode;
    }



    /**
     * A wrapper to contain the A* meta information about the Nodes
     * <p/>
     */
    class AStarNodeWrapper
    {

        // the underlying Node associated with the metainformation
        Node node;

        // the Node from which this Node was most profitably linked

        AStarNodeWrapper cameFrom;


        double gx, hx, fx;


        public AStarNodeWrapper(Node n)
        {
            node = n;
            gx = 0;
            hx = 0;
            fx = 0;
            cameFrom = null;
        }


    }
}